![]() |
![]() |
|
Das Projekt enthält zusätzlich zu Main noch die Definition der Methode Worker in ClassA. In Main werden zwei Threads konstruiert, die beide die Methode Worker aufrufen. Worker selbst durchläuft eine Schleife, in der die Variable intVar hochgezählt und der aktuelle Inhalt an der Konsole ausgegeben wird. Mit dem Endwert von 100 wird die Methode wieder verlassen. Beide Threads greifen auf dasselbe Objekt zu und teilen sich die Arbeit mehr oder weniger abwechselnd, um das Feld intVar hoch zu zählen und dessen Inhalt anzuzeigen. Eigentlich sollte man erwarten, dass die Zahlen chronologisch hintereinander ausgegeben werden, jedoch kommt es an der Konsole beispielsweise zu folgender Ausgabe:
Beide Threads greifen unsynchronisiert auf die Variable intVar zu, wobei die Operation des ersten Threads mitten in der Schleife unterbrochen wird. Dieses ist dem Anschein nach genau der Moment, nachdem der Feldinhalt mit der Anweisung
zwar schon auf 40 erhöht, aber mit
noch nicht an der Konsole ausgegeben wurde. Der unterbrochene Thread weiß natürlich genau, mit welcher Anweisung er seine Arbeit wieder aufnehmen muss, wenn ihm der Scheduler wieder Prozessorzeit zuteilt: Er muss zuerst die Zahl 40 ausgeben. Diesen Zwischenstand, dessen Informationen durch den Inhalt der CPU-Register beschrieben werden, speichert das System im Stack und räumt daraufhin den Prozessor für den nächsten Thread in der Warteschlange. Der zweite Thread, dem anschließend die CPU zugeteilt wird, tritt nun seinerseits zum ersten Mal in die Schleife ein, erkennt den aktuell gültigen Feldinhalt der Variablen (er beträgt 40), erhöht diesen zunächst auf 41, gibt den Wert aus und setzt die Schleife so lange fort, bis seine Zeit abgelaufen ist. Dann verlässt der zweite Thread die CPU, das System liest die im Stack gesicherten Daten des ersten Threads in die CPU ein und setzt die Arbeit mit genau der Anweisung fort, bei der er unterbrochen wurde: die Ausgabe der Zahl 40 an der Konsole. 11.3.2 Der »Monitor« zur Synchronisation
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| intVar++; |
| Console.WriteLine(intVar); |
Die Ausgabe wird nur dann unseren Erwartungen entsprechen, wenn ein laufender Thread seine Ausführung nicht zwischen diesen beiden Anweisungen unterbrechen muss, denn zur Erhöhung des Feldwertes gehört auch die Anzeige an der Konsole. Dieser Zusammenhang muss für jeden der beiden Threads ersichtlich sein.
| Hinweis Einige Operationen können auch bei einem bevorstehenden Wechsel der Zeitscheibe nicht unterbrochen werden. Solche Operationen werden als atomare Operationen bezeichnet. Dazu gehören beispielsweise einfache Schreib- und Lesevorgänge auf Integerzahlen, während die gleichen Vorgänge auf Dezimalzahlen nicht atomar sind. Auch Operationen mit den Operatoren ++ und – sind nicht atomar. Deshalb kann es auch zu einem Zeitscheibenwechsel mitten in der Ausführung von intVar++ kommen. |
An dieser Stelle kommt eine neue Klasse ins Spiel, welche die Aufgabe einer Synchronisation übernimmt: Monitor. Mit dieser Klasse lässt sich verhindern, dass mehrere Threads gleichzeitig einen bestimmten Codeteil im Programm durchlaufen. Mit anderen Worten bedeutet das, dass zu einem Zeitpunkt immer nur ein Thread dieses Codesegment durchlaufen kann. Andere Threads, die ebenfalls dieses Codesegment ausführen wollen, müssen warten, bis der laufende Thread das Codesegment verlassen hat.
Mit den Methoden Enter und Exit der Klasse Monitor können kritische Codeabschnitte definiert werden, die zu einem gegebenen Zeitpunkt nur von einem Thread betreten werden dürfen. Mit Enter wird das Codesegment so lange blockiert, bis die Sperrung mit Exit wieder aufgehoben wird. Damit sind ungültige Zustände, die ein Thread hinterlassen könnte, wenn ihm die Zeitscheibe entzogen wird, nicht mehr möglich. Monitor protokolliert, ob der Vorgängerthread den kritischen Abschnitt mit Exit ordnungsgemäß verlassen hat oder nicht.
Sowohl Enter als auch Exit sind statische Methoden der Klasse Monitor. Als Argument wird den beiden Methoden die Referenz auf das zu synchronisierende Objekt übergeben, das auch this sein darf.
| public static void Enter(object obj); |
| public static void Exit(object obj); |
Wir ändern jetzt das Beispiel oben und schaffen die Voraussetzung, dass die Zugriffe auf die kritischen Anweisungen synchronisiert erfolgen. Zur Bestätigung lassen wir uns diesmal zusätzlich noch den Hashcode des jeweiligen Threads ausgeben, der die angezeigte Zahl erzeugt hat.
| // ------------------------------------------------------------- |
| // Beispiel: ...\Kapitel 11\SynchronisierteThreads |
| // ------------------------------------------------------------- |
| ... |
| class ClassA { |
| private int intVar; |
| public void Worker() { |
| while(true) { |
| // Sperre setzen |
| Monitor.Enter(this); |
| intVar++; |
| if (intVar > 100) break; |
| Console.WriteLine("Zahl = {0,5} Thread = {1,3}", intVar, Thread.CurrentThread.GetHashCode().ToString()); |
| Thread.Sleep(5); |
| // Sperre aufheben |
| Monitor.Exit(this); |
| } |
| } |
| } |
Nun erhalten wir wunschgemäß die Ausgabe der chronologisch geordneten Zahlen von 1 bis 100.
Neben der Enter-Methode gibt es in der Monitor-Klasse noch die Methode TryEnter. Diese überprüft zuerst, ob der geschützte Codeabschnitt frei ist, sperrt ihn dann und führt den Code aus. Ist das Codesegment gesperrt, liefert TryEnter den Rückgabewert false. Darauf kann der Entwickler entsprechend reagieren. Der Rückgabewert kann beispielsweise von einem if-Statement sinnvoll ausgewertet werden.
Neben Enter und Exit der Klasse Monitor gibt es noch eine andere, sprachspezifische Möglichkeit, den Zugriff zu synchronisieren. Unter C# ist das die lock-Anweisung. Die Syntax dazu lautet:
| lock (Ausdruck) |
| { |
| // zu synchronisierende Anweisungen |
| } |
Durch den Anweisungsblock hinter lock werden die Anweisungen eingeschlossen, die es zu synchronisieren gilt. Dieses Statement ist sehr einfach zu handhaben, aber es besitzt nicht die Möglichkeiten, mit denen die Klasse Monitor ausgestattet ist.
Wir wollen im folgenden Beispiel lock einsetzen und dabei gleichzeitig die Frage beantworten, wie sich die Synchronisation einer Methode auf die anderen Methoden desselben Objekts auswirkt, die nicht synchronisiert sind. Dazu entwickeln wir eine Klasse ClassA mit den Methoden SyncProc und UnsyncProc. Wie der Bezeichner schon zum Ausdruck bringt, unterstützt nur erstere den synchronisierten Zugriff.
| // -------------------------------------------------------------- |
| // Beispiel: ...\ Kapitel 11\Lock-Anweisung |
| // -------------------------------------------------------------- |
| class Program { |
| static void Main(string[] args) { |
| ClassA obj = new ClassA(); |
| Thread thread1, thread2; |
| thread1 = new Thread(new ThreadStart(obj.SyncLock)); |
| thread2 = new Thread(new ThreadStart(obj.UnsyncLock)); |
| thread1.Start(); |
| thread2.Start(); |
| Console.ReadLine(); |
| } |
| } |
| class ClassA { |
| // synchronisierte Methode |
| public void SyncLock() { |
| lock(this) { |
| for(int i = 0; i <= 100; i++) { |
| Thread.Sleep(20); |
| Console.Write(" {0} ", i); |
| } |
| } |
| } |
| // unsynchronisierte Methode |
| public void UnsyncLock() { |
| for(int i = 0; i <= 50; i++) { |
| Thread.Sleep(20); |
| Console.Write("..STOP.."); |
| } |
| } |
| } |
Die folgende Abbildung zeigt die Ausgabe an der Konsole.

Hier klicken, um das Bild zu vergrößern
Abbildung 11.9 Ausgabe des Beispiels »Lock-Anweisung«
In SyncProc wird in einer Schleife der Schleifenzähler nach dem Aufruf der Sleep-Methode an der Konsole ausgegeben, in UnsyncProc die ebenfalls zeitverzögerte Zeichenfolge ..STOP.. –. Würde sich die Synchronisation auf alle Threads auswirken, die sich der Methoden desselben Objekts bedienen, müsste der Zähler von 0 bis 100 ununterbrochen hintereinander an der Konsole erscheinen.
Tatsächlich ist es aber so, dass sich die unsynchronisierte Methode völlig unbeeindruckt davon zeigt, dass ein anderer Thread auf die synchronisierte zugreift – die Synchronisation bezieht sich nur auf die Anweisungen innerhalb des zur Synchronisation gekennzeichneten Blocks.
Vielleicht haben Sie sich schon die Frage gestellt, warum sich der Ein- und der Austrittsanweisung in den Monitor eine Objektreferenz anschließt, beispielsweise:
| Monitor.Enter(this); |
Das legt die Vermutung nahe, dass es zu einem gegebenen Objekt auch nur einen Monitor gibt, oder mit anderen Worten: Es kann auf ein Objekt nur eine Sperre gelegt werden. Der Code im vorherigen Beispiel fordert geradezu heraus, entsprechend geändert zu werden, um diese Vermutung bestätigt zu sehen. Wir ändern dazu zunächst die Namen der Methoden – denn auf eine Methode mit dem Namen UnsyncProc eine Synchronisation erzwingen zu wollen, würde nur zu Missverständnissen führen. In der umbenannten Methode fügen wir dann noch den Monitor hinzu (es könnte auch gleichwertig die lock-Anweisung benutzt werden). Nach den Änderungen sieht der Beispielcode wie folgt aus:
| // -------------------------------------------------------------- |
| // Beispiel: ...\ Kapitel 11\Lock-Anweisung 2 |
| // -------------------------------------------------------------- |
| ... |
| class ClassA { |
| // 1. synchronisierte Methode |
| public void SyncLock1() { |
| lock(this) { |
| for(int i = 0; i <= 100; i++) { |
| Thread.Sleep(20); |
| Console.Write(".{0}.", i); |
| } |
| } |
| } |
| // 2. synchronisierte Methode |
| public void SyncLock2() { |
| lock(this) { |
| for(int i = 0; i <= 50; i++) { |
| Thread.Sleep(20); |
| Console.Write("..STOP.."); |
| } |
| } |
| } |
| } |
Wenn wir die auf diese Weise veränderte Anwendung starten, sehen wir uns bestätigt: Die Ergebnisse aus den beiden Anweisungsblöcken des lock-Statements werden jeweils zusammenhängend ausgegeben.
| Der Monitor kann für ein bestimmtes Objekt nur ein einziges Mal vergeben werden. Greifen mehrere Threads auf mehrere synchronisierte Methoden desselben Objekts zu, muss der Thread, der als erster die Sperre erhalten hat, seine Operationen vollständig abschließen, bevor der zweite Thread das Recht auf den Monitor erhält. |
Daraus kann eine Schlussfolgerung gezogen werden: Wenn Sie zwei Methoden haben, die eine Synchronisation erfordern und dabei nicht voneinander abhängen, sollten Sie diese Methoden in separaten Klassen implementieren.
Damit ist auch erklärt, weshalb den Methoden Enter und Exit der Klasse Monitor eine Objektreferenz übergeben wird: Die Sperre bezieht sich auf das gesamte Objekt.
Die Klasse Monitor ist nicht instanziierbar, da jedem Objekt nur ein Monitor zugeordnet werden kann. Mehrere Objekte können den Anspruch auf die Nutzung des Monitors eines anderen Objekts erheben, aber nur einem Objekt aus der Warteschlange wird er zugestanden.
Stellen Sie sich den Monitor wie ein Fernglas vor, das Sie mit in den Urlaub genommen haben, um damit die Landschaft aus der Nähe zu betrachten. Solange Sie das Fernglas benutzen, hat keine andere Person die Möglichkeit, die schönen Dinge der Natur aus der Nähe zu betrachten. Eine andere Person, die auch einen Blick durch das Fernglas werfen möchte, wird sich in die Warteschlange einreihen müssen. Erst wenn Sie das Fernglas zur Seite gelegt haben, kann es von einer Person aus der Warteschlange aufgenommen werden. Alle anderen Personen müssen sich weiter gedulden.
Nehmen wir jetzt an, Sie wären mit einem Ihrer Freunde im Urlaub. Während Sie durch das Fernglas schauen, erhebt auch Ihr Freund darauf Anspruch. Sie legen das Fernglas freiwillig zur Seite, informieren Ihren Freund darüber, dass er es nun benutzen darf, und treten freiwillig in die Warteschlange.
Die letzten beiden Aktionen lassen sich auch auf den Monitor projizieren. Sobald Sie das Fernglas zur Seite legen mit der Absicht, es zu einem späteren Zeitpunkt noch einmal aufzugreifen, versetzen Sie sich in den Wartezustand und begeben sich in die Warteschlange. Die Monitor-Klasse beschreibt diese Operation mit der statischen Methode Wait. Das Informieren des nächsten Interessenten in der Warteschlange entspricht der ebenfalls statischen Methode Pulse. Beide Methoden können nur innerhalb eines Synchronisationsblocks aufgerufen werden.
Mit Wait wird der aktuelle Thread blockiert und gleichzeitig die Sperrung des Objekts aufgehoben. Damit kann ein anderer Thread das freigegebene Objekt nutzen. Schauen wir uns eine Definition der überladenen Wait-Methode an:
| public static bool Wait(object obj); |
Der Parameter nimmt die Referenz auf das Objekt entgegen, dessen Sperrung aufgehoben werden soll. Ein wenig sonderbar verhält sich der Rückgabewert. Er ist true, wenn kein anderer Thread das Objekt sperrt und der aktuelle Thread selbst die Verantwortung der Sperrung übernimmt. Ansonsten kommt kein boolescher Wert zurück, was eine Einreihung in die Warteschlange zur Folge hat. Damit bietet sich Wait auch dazu an, als Bedingung für den Eintritt in eine Schleife behilflich zu sein:
| while(Monitor.Wait(obj)) { |
| // Thread tritt in den synchronisierten Block ein |
| } |
Es besteht ein großer Unterschied zwischen einem Thread, der mit Enter auf den Eintritt in eine synchronisierte Methode wartet, und einem Thread, der sich mit Wait in den Wartezustand versetzt hat. Ein Thread, der eine synchronisierte Methode mit Enter betreten möchte, befindet sich im Zustand bereit. Er reiht sich in die Threads ein, die auf Anweisung des Schedulers hin ein Segment der Zeitscheibe erhalten. Ein Thread, der mit Wait die Sperrung eines Objekts aufgehoben hat, befindet sich in einer Warteliste. Allerdings nicht in die Warteliste, aus welcher der Scheduler einem bereiten Thread die CPU zuteilt, sondern eine Warteliste aller derer Threads, die durch den Zustand wartend beschrieben werden.
Um einen Thread aus seinem Wartezustand zu holen, muss ein anderer Thread die Methode Pulse oder PulseAll auf dem gesperrten Objekt aufrufen. Das Problem ist, dass Pulse keinen bestimmten wartenden Thread aus der Liste holt, sondern – falls sich mehrere Threads darin befinden – einen mehr oder weniger willkürlich gewählten, während mit PulseAll alle Threads den Zustand wartend aufgeben und in bereit übergehen. Damit stehen sie wieder in der Warteschlange der Zeitscheibe – der Scheduler kann ihnen wieder Prozessorzeit zuteilen.
Ein Thread, der mit Wait die Sperrung des kritischen Codebereichs aufgehoben hat, wartet auf einen Anstoß von außen, um wieder aktiv werden zu können. Er selbst hat keine Möglichkeit, diesen Zustand zu beenden. Wenn kein anderer Thread Pulse oder PulseAll aufruft, wird ein wartender Thread daher niemals mehr laufen können.
| Im Extremfall kann der Wartezustand der Threads einer Anwendung zu einem Phänomen führen, das unter der Bezeichnung Deadlock bekannt ist. Dabei befinden sich ausnahmslos alle Threads im blockierten Wartezustand. Die Anwendung hängt sich in diesem Moment auf. |
Wir wollen nun die vorgestellten Methoden in einem Beispiel testen. Dazu werden wir ein Programm entwickeln, das in Lage ist, Zahlen zu erzeugen. Das ist eigentlich nichts Weltbewegendes, und wir haben es auch schon in den anderen Beispielen gemacht. Das Besondere ist jedoch, dass jede Zahl genau einmal von einem Verbraucher ausgewertet werden soll. Der Verbrauch soll durch eine Konsolenausgabe simuliert werden. Erzeuger und Konsument sollen in einem eigenen Thread laufen.
| // -------------------------------------------------------------- |
| // Beispiel: ...\ Kapitel 11\Zahlenkonsument |
| // -------------------------------------------------------------- |
| class Program { |
| public static bool finished = false; |
| public static bool thread1Waiting = false; |
| public static bool thread2Waiting = false; |
| static void Main(string[] args) { |
| MyNumber zahl = new MyNumber(); |
| ProduceNumber prod = new ProduceNumber(zahl); |
| ConsumeNumber cons = new ConsumeNumber(zahl); |
| Thread thread1, thread2; |
| // Threads instanziieren |
| thread1 = new Thread(new ThreadStart(prod.MakeNumber)); |
| thread2 = new Thread(new ThreadStart(cons.GetNumber)); |
| // Threads starten |
| thread1.Start(); |
| thread2.Start(); |
| Console.ReadLine(); |
| } |
| } |
| // ------ erzeugt eine Zahl ------------ |
| class ProduceNumber { |
| private MyNumber obj; |
| public ProduceNumber(MyNumber obj) { |
| this.obj = obj; |
| } |
| public void MakeNumber() { |
| Random rnd = new Random(); |
| Monitor.Enter(obj); |
| for (int i = 0; i <= 10; i++) { |
| Program.thread1Waiting = true; |
| // falls Konsumer-Thread noch nicht im Wartezustand, |
| // selbst in den Wartezustand gehen |
| if(Program.thread2Waiting == false) |
| Monitor.Wait(obj); |
| obj.Number = rnd.Next(0, 1000); |
| Console.WriteLine("Nummer {0} erzeugt", obj.Number); |
| // dem nächsten in der Warteschlange stehenden Objekt |
| // den Monitor übergeben |
| Monitor.Pulse(obj); |
| Program.thread2Waiting = false; |
| } |
| Program.finished = true; |
| Monitor.Exit(obj); |
| } |
| } |
| // --------- verbraucht eine Zahl ------------- |
| class ConsumeNumber { |
| private MyNumber obj; |
| public ConsumeNumber(MyNumber obj) { |
| this.obj = obj; |
| } |
| public void GetNumber() { |
| Monitor.Enter(obj); |
| // wenn sich der Erzeugerthread im Wartezustand |
| // befindet, ihn 'bereit' schalten |
| if(Program.thread1Waiting) |
| Monitor.Pulse(obj); |
| Program.thread2Waiting = true; |
| while(Monitor.Wait(obj)) { |
| Console.WriteLine("Nummer {0} verbraucht",obj.Number); |
| Monitor.Pulse(obj); |
| if(Program.finished) Thread.CurrentThread.Abort(); |
| } |
| Monitor.Exit(obj); |
| } |
| } |
| // ------------ repräsentiert eine Zahl ------------------- |
| class MyNumber { |
| private int intValue; |
| public int Number { |
| get {return intValue;} |
| set {intValue = value;} |
| } |
| } |
Der Kern der Anwendung wird durch die beiden Klassen ProduceNumber und ConsumeNumber beschrieben. ProduceNumber erzeugt mit der Methode MakeNumber auf Basis des Zufallszahlengenerators Zahlen zwischen 0 und 999 und schreibt diese in das Feld eines Objekts vom Typ der Klasse MyNumber, das in Main erzeugt wird und dessen Referenz den Konstruktoren der Klassen ConsumeNumber und ProduceNumber übergeben wird. Damit ist sichergestellt, dass sowohl Erzeuger als auch Verbraucher mit demselben MyNumber-Objekt operieren.
Betrachten wir die prinzipielle Arbeitsweise des Verbrauchers und des Konsumenten unter der Prämisse, dass zuerst der Erzeugerthread und danach der Verbraucherthread gestartet wird. Der gesamte Code in der Routine MakeNumber ist synchronisiert. Insgesamt werden elf Zahlen in einer Schleife erzeugt. Direkt nach dem Schleifeneintritt wird die Wait-Methode des Monitors aufgerufen und die Sperre des Objekts vom Typ MyNumber aufgehoben. Jetzt kann ein anderer Thread auf das freigegebene MyNumber-Objekt zugreifen und die Zahl »verbrauchen«, die einen Schleifendurchlauf zuvor erzeugt worden ist. Der Erzeugerthread verharrt so lange in Wartestellung, bis der Verbraucherthread Pulse aufruft und den Erzeugerthread wieder in den Zustand bereit versetzt. Ist dessen Wartezustand aufgehoben, wird die nächste Zahl erzeugt. Bevor der nächste Schleifendurchlauf ausgeführt wird, wird der Verbraucher mit Pulse in den Zustand bereit erhoben.
In der Methode GetNumber des Verbrauchers ist der Programmcode ebenfalls synchronisiert. Direkt nach dem Eintritt in den Synchronisierungsabschnitt wird Pulse aufgerufen, um den wartenden Erzeugerthread nach dem Start der Anwendung in den Zustand bereit zu versetzen. Anschließend ruft der Konsument Wait auf. Damit wird der Monitor des Objekts an den Erzeuger weitergegeben, der eine Zahl erzeugt. Gibt der Erzeuger die Sperre an den Konsumenten zurück, wird die neue Zahl zuerst an der Konsole angezeigt und anschließend Pulse aufgerufen. Jetzt ist der Erzeugerthtread wieder im Zustand bereit, während der Konsument seinerseits anschließend den Monitor freigibt und sich in den Wartezustand versetzt.
Damit der Verbraucherthread überhaupt erfährt, wann der Erzeuger die letzte Zahl bereitgestellt hat, ist die boolesche Variable finished in Class1 deklariert, die vom Erzeuger nach dem letzten Schleifendurchlauf auf true gesetzt wird.

Hier klicken, um das Bild zu vergrößern
Abbildung 11.10 Die Ausgabe des Projekts »Zahlenkonsument«
So weit zur prinzipiellen Arbeitsweise. Es gibt aber noch ein Problem, dem wir bisher noch keine Beachtung geschenkt haben: Wir können nämlich nicht garantieren, dass der Erzeugerthread als Erstes gestartet wird. Erhält nach dem Starten der Anwendung der Konsument vor dem Erzeuger die CPU, würde es zu einem klassischen Deadlock kommen, wenn sich beide Threads gleichzeitig im Zustand warten befinden. Wir müssen also eine genaue Steuerung des Programmablaufs in der Weise erzwingen, dass sich der Verbraucherthread im Wartezustand befindet, wenn die erste Zahl erzeugt wird. Diese Steuerung wird über die beiden booleschen Variablen thread1Waiting und thread2Waiting erreicht, deren Auswertung garantiert, dass sich – unabhängig von der Startreihenfolge – zu einem gegebenen Zeitpunkt immer nur ein Thread im Wartezustand befindet.
Die Klasse Monitor eignet sich nur zur Synchronisation von Threads, die innerhalb eines Prozessraums laufen. Manchmal müssen Abläufe aber auch über Prozessgrenzen hinweg synchronisiert werden. In diesen Fällen müssen Sie die Klasse Mutex einsetzen. Ich möchte Ihnen hierzu ein Beispiel zeigen, bei dem ein Mutex-Objekt dazu benutzt wird zu verhindern, dass eine Anwendung mehrfach gestartet werden kann.
| // ------------------------------------------------------------ |
| // Beispiel: ...\Kapitel 11\MutexDemo |
| // ------------------------------------------------------------ |
| class Program { |
| private static Mutex mutex; |
| static void Main(string[] args) { |
| if (IsApplicationStarted()) { |
| Console.WriteLine("Die Anwendung wurde bereits gestartet"); |
| Console.WriteLine("Ein zweiter Start ist nicht möglich."); |
| } |
| else { |
| Console.WriteLine("Die Anwendung wird gestartet."); |
| Console.WriteLine("Die Anwendung läuft."); |
| } |
| Console.ReadLine(); |
| } |
| public static bool IsApplicationStarted() { |
| string mutexName = Application.ProductName; |
| mutex = new Mutex(false, mutexName); |
| if (mutex.WaitOne(0, true)) |
| return false; |
| else |
| return true; |
| } |
| } |
Ein Mutex ist ein einfaches Systemobjekt und durch einen eindeutigen Namen gekennzeichnet. Ein Mutex-Objekt gestattet nur jeweils einem Thread exklusiven Zugriff auf die gemeinsam genutzte Ressource. In unserem Beispiel wird das die Anwendung selbst sein. Wenn ein Thread ein Mutex-Objekt erhält, wird ein zweiter Thread, der dieses Objekt abruft, so lange angehalten, bis der erste Thread das Mutex-Objekt freigibt.
Die Klasse Mutex stellt mehrere Konstruktoren zur Verfügung. Für unsere Belange ist der geeignet, dem wir den Namen des Mutex mitteilen können. Die Schwierigkeit besteht bei der Namensvergabe darin, dass er systemeindeutig sein muss, um den Mutex identifizieren zu können. Es bietet sich hier der Anwendungsname an, obwohl dieser auch keine Garantie gibt. Möglicherweise müssen Sie hier noch Zusatzinformationen hinzufügen, um die Eindeutigkeit zumindest mit hoher Wahrscheinlichkeit zu gewährleisten. Der entsprechende Konstruktor erwartet darüber hinaus auch noch einen booleschen Wert, der angibt, ob dem aufrufenden Thread der anfängliche Besitz des Mutex zugewiesen werden soll. Er ist true, um dem aufrufenden Thread den anfänglichen Besitz des benannten Mutex zuzuweisen.
Über die Methode WaitOne kann eine Ressource das Mutex-Objekt anfordern. Ist der Rückgabewert true, ist das Objekt nicht im Besitz eines anderen Threads, ansonsten wäre der Rückgabewert false.
WaitOne werden zwei Argumente übergeben: Das erste beschreibt ein Zeitintervall, das angibt, wie lange auf ein Signal gewartet werden soll. Über das zweite Argument können Sie bei Anwendungen, die vor dem Warten auf den Mutex den Zugriff auf Objekte oder Klassen über lock sperren festlegen, dass die Sperrung vor dem Warten aufgehoben und nach dem Warten wieder gesetzt wird. Für uns hat dieser Parameter keine Bedeutung, wir setzen ihn auf true.
Im Beispielprogramm dient die Methode IsApplicationStarted dazu zu prüfen, ob das Mutex-Objekt sich bereits im Besitz eines anderen Threads befindet. Je nachdem, wie das Ergebnis der Prüfung ausfällt, wird eine entsprechende Konsolenausgabe erscheinen.
Es gibt noch eine weitere Alternative, Synchronisierung zwischen mehreren Threads zu erzielen: mittels des Attributs MethodImpl. Die zugrunde liegende Klasse ist im Namespace System.Runtime.CompilerServices zu finden. Das Attribut kann nur auf Konstruktoren und Methoden angewendet werden. Es ersetzt die Klasse Monitor mit dem Unterschied, dass nicht nur ein bestimmtes Codesegment eingehüllt wird, sondern in einem Zug gleich die gesamte Methode. Somit kann auch nur immer ein Thread gleichzeitig diese Methode ausführen. Dazu ein Beispiel:
| [MethodImpl(MethodImplOptions.Synchronized)] |
| public void Calculate() { |
| // Anweisungen |
| } |
Dem Attribut MethodImpl können verschiedene Parameter übergeben werden. Zur Synchronisation verwenden Sie MethodImplOptions.Synchronized.
| << zurück |
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.